package edu.tum.cs.stageflow.language.jvmmodel

import com.google.inject.Inject
import edu.tum.cs.stageflow.language.stageflow.Flow
import edu.tum.cs.stageflow.language.stageflow.Port
import edu.tum.cs.stageflow.language.stageflow.Stage
import edu.tum.cs.stageflow.language.stageflow.Step
import edu.tum.cs.stageflow.language.stageflow.Sub
import org.eclipse.xtext.common.types.util.Primitives
import org.eclipse.xtext.common.types.util.TypeReferences
import org.eclipse.xtext.naming.IQualifiedNameProvider
import org.eclipse.xtext.xbase.XVariableDeclaration
import org.eclipse.xtext.xbase.compiler.TypeReferenceSerializer
import org.eclipse.xtext.xbase.compiler.output.ITreeAppendable
import org.eclipse.xtext.xbase.jvmmodel.AbstractModelInferrer
import org.eclipse.xtext.xbase.jvmmodel.IJvmDeclaredTypeAcceptor
import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder
import org.eclipse.xtext.xbase.lib.Procedures
import edu.tum.cs.stageflow.language.stageflow.StagePerformance
import org.eclipse.xtext.common.types.JvmVisibility

/**
 * <p>Infers a JVM model from the source model.</p> 
 *
 * <p>The JVM model should contain all elements that would appear in the Java code 
 * which is generated from the source model. Other models link against the JVM model rather than the source model.</p>     
 */
class StageflowJvmModelInferrer extends AbstractModelInferrer {

	@Inject extension JvmTypesBuilder
	@Inject extension IQualifiedNameProvider
	@Inject extension Primitives
	@Inject extension TypeReferenceSerializer
	@Inject TypeReferences typeReferences

	def dispatch void infer(StagePerformance stageP, IJvmDeclaredTypeAcceptor acceptor, boolean isPreIndexingPhase) {
		acceptor.accept(stageP.toClass(stageP.stage.fullyQualifiedName + '''_''' + stageP.counter)).initializeLater(
			[
				members += stageP.toField('''benchmark''', stageP.newTypeRef('''edu.tum.cs.stageflow.executionframework.BenchmarkResult''') ) [
					initializer = '''null'''
					setStatic(true)
					setVisibility(JvmVisibility.PRIVATE)
				]
				members += stageP.toConstructor [ ]
				
				members += stageP.toMethod('''getBenchmark''', stageP.newTypeRef('''edu.tum.cs.stageflow.executionframework.BenchmarkResult''')) [
					val Procedures.Procedure1<ITreeAppendable> functionCode = [
							append('''if(benchmark==null) {''')
							newLine
							append('''benchmark = new edu.tum.cs.stageflow.executionframework.BenchmarkResult("«stageP.stage.name»", 
							«stageP.counter»,
							«stageP.startT»,
							«stageP.readT»,
							«stageP.execT»,
							«stageP.writeT»,
							«stageP.stopT»); }''')
							newLine
							append('''return benchmark;''')
					]
					body = functionCode
				]
			])
	}

	def dispatch void infer(Stage stage, IJvmDeclaredTypeAcceptor acceptor, boolean isPreIndexingPhase) {
		val queueType = '''edu.tum.cs.stageflow.executionframework.Flow'''
		val upperExecBoarder = 100000000;
		acceptor.accept(stage.toClass(stage.fullyQualifiedName)).initializeLater(
			[
				documentation = stage.documentation
				/* Cache safe features so that syntax issues cannot cause update problems with NPEs and proxies. */
				val safePorts = stage.ports.filter[name != null && type != null]
				val safeSubs = stage.subs.filter[it.stage != null && !it.stage.eIsProxy]
				val safeFlows = stage.flows.filter[src != null && snk != null]
				val safeVars = stage.vars.filter(XVariableDeclaration).filter[name != null]
				/* Add type parameters. */
				for (p : stage.typeParams) {
					typeParameters.add(p.cloneWithProxies)
				}
				/* derive from IStage */
				superTypes.add(stage.newTypeRef("edu.tum.cs.stageflow.executionframework.IStage"))
				/* add counter */
				members += stage.toField('''counter''', stage.newTypeRef('''long''')) [
					initializer = '''0L'''
				]
				/* add finisher */
				members += stage.toField('''isFinished''', stage.newTypeRef('''boolean''')) [
					initializer = '''false'''
				]
				/* Add stage constructor parameters as final fields. */
				for (p : stage.params) {
					val field = p.toField(p.name, p.parameterType)
					field.documentation = p.documentation
					field.final = true
					members += field
				}
				/* Add internal state variables as fields. */
				for (v : safeVars) {
					if (v instanceof XVariableDeclaration) {
						val field = v.toField(v.name, v.type)[initializer = v.right]
						field.documentation = v.documentation
						field.final = !v.writeable
						members += field
					}
				}
				/* Add ports as fields with public getters and setters. */
				for (p : safePorts) {
					val fieldType = p.type
					members += p.toField(p.name, fieldType)[documentation = p.documentation]
					members += p.toGetter(p.name, fieldType)
					members += p.toSetter(p.name, fieldType)
					members +=
						stage.toField(p.name + '''_flow''', p.newTypeRef('''«queueType»<«fieldType.identifier»>'''))
				}

				/* Add Runner to members */
				members +=
					stage.toField('''runner''',
						stage.newTypeRef('''edu.tum.cs.stageflow.executionframework.StageRunner'''))
				members +=
					stage.toGetter('''runner''',
						stage.newTypeRef('''edu.tum.cs.stageflow.executionframework.StageRunner'''))
				/* Add constructor  */
				members += stage.toConstructor [
					/* Add stage parameters as constructor parameters. */
					for (p : stage.params) {
						parameters += p.toParameter(p.name, p.parameterType)
					}
					/*Add Blocking Queue as Stage Parameter */
					for (p : safePorts) {
						parameters += p.toParameter(p.name, p.newTypeRef('''«queueType»<«p.type.identifier»>'''))
					}
					/* Generate custom constructor initialization code for parameters and subs. */
					val Procedures.Procedure1<ITreeAppendable> constructorCode = [
						/* Parameters */
						append('''«FOR p : stage.params SEPARATOR "\n"»this.«p.name» = «p.name»;«ENDFOR»''')
						newLine
						for (p : safePorts) {
							newLine
							val queue = p.name + '''_flow'''
							append('''«queue» = «p.name»;''')
						}
						/* Instantiate queues */
						for (f : safeFlows) {
							newLine
							//val typeIdentifier = f.src.port.type. identifier
							val typeIdentifier = f.src.port.type.getQualifiedName()
							var queue_name = findQueue(f, stage, safePorts)
							if (!isSubFlow(f, safePorts)) {
								append(
									'''«queueType»<«typeIdentifier»> «queue_name» = new «queueType»<«typeIdentifier»>();''')
							}
							newLine
						}
						newLine
						/* Instantiate Subs + assign queues */
						for (s : safeSubs) {
							newLine
							append('''this.«s.name» = new «s.stage.name»(''')
							val c = Math.min(s.args.size, s.stage.params.size)
							append('''«FOR i : 0 ..< c SEPARATOR ", "»«s.argumentFieldName(i)»«ENDFOR»''')

							var b = false
							if(c > 0) b = true

							for (param : s.stage.params) {
								if(b) append(''', ''')
								append(param.name)
								b = true
							}
							for (f : safeFlows) {
								if (f.src.sub == s || f.snk.sub == s) {
									if(b) append(''', ''')
									var queue_name = findQueue(f, stage, safePorts)
									append(queue_name)
									b = true
								}
							}
							append(");")
						}
						newLine
						append('''runner = new edu.tum.cs.stageflow.executionframework.StageRunner(this);''')
					]
					body = constructorCode
				]
				/* Add steps as methods. */
				var j = 0
				for (s : stage.steps) {
					members += s.toMethod(s.stepMethodName(j), null)[body = s.expression]
					j = j + 1
				}
				/* Add inherited methods. */
				/* read */
				members += stage.toMethod('''readInputs''', stage.newTypeRef("void")) [
					val Procedures.Procedure1<ITreeAppendable> functionCode = [
						for (p : safePorts) {

							/* get all input values */
							if (p.inbound) {
								newLine
								append('''synchronized(«p.name»_flow) {''')
								newLine
								append('''while («p.name»_flow.isEmpty()) {''')
								newLine
								append('''if(«p.name»_flow.isFinished()) { isFinished = true; return; }''')
								newLine
								append('''try {''')
								newLine
								append('''«p.name»_flow.wait(); }''')
								newLine
								append(''' catch (InterruptedException e) { e.printStackTrace(); }}''')
								newLine
								append('''«p.name» = «p.name»_flow.remove();''')
								newLine
								append('''«p.name»_flow.notifyAll(); }''')
							}
							/* reset all output values */
							else {
								newLine
								append('''«p.name» = null;''')
							}
						}
						/* insert values from in ports into sub queues */
						for (f : safeFlows) {
							if (f.src.sub == null && safePorts.exists[it.equals(f.src.port)]) {
								newLine
								var queue_name = findQueue(f, stage, safePorts)
								var variable_name = '''«f.src.port.name»'''
								append('''try {''')
								newLine
								append('''if(«variable_name» != null) {«queue_name».put(«variable_name»);}''') //TODO change according to "writeOutput" (deadlock)
								newLine
								append('''} catch (InterruptedException e) {e.printStackTrace();}''')
							}
						}
					]
					body = functionCode
				//annotations += stage.toAnnotation('''@Override''')
				]
				/* execute */
				members += stage.toMethod('''execute''', stage.newTypeRef("void")) [
					val Procedures.Procedure1<ITreeAppendable> functionCode = [
						var k = 0
						for (s : stage.steps) {
							newLine
							append('''«s.stepMethodName(k)»();''')
							k = k + 1
						}
						for (s : safeSubs) {
							newLine
							append('''Thread «s.name»_thread = this.«s.name».getRunner();''')
							newLine
							append('''«s.name»_thread.start();''')
						}
						newLine
						append('''this.counter++;''')
						newLine
						if(!safeSubs.empty) {
							append('''try {''')
							for (s : safeSubs) {
								newLine
								append('''«s.name»_thread.join();''')
							}
							append('''} catch (InterruptedException ex) { }''')
						}
					]
					body = functionCode
				//annotations += stage.toAnnotation('''@Override''')
				]
				/* write */
				members += stage.toMethod('''writeOutputs''', stage.newTypeRef("void")) [
					//TODO Read Outputs first!!!!
					val Procedures.Procedure1<ITreeAppendable> functionCode = [
						for (f : safeFlows) {
							if (f.snk.sub == null && safePorts.exists[it.equals(f.snk.port)]) {
								newLine
								var queue_name = findQueue(f, stage, safePorts)
								var variable_name = '''«f.snk.port.name»'''
								append('''try {''')
								newLine
								append('''«variable_name» = «queue_name».take();''') //TODO change according to "readInputs" (deadlock)
								newLine
								append('''} catch (InterruptedException e) {e.printStackTrace();}''')
							}
						}
						for (p : safePorts) {
							if (p.inbound) {
								newLine
								append('''«p.name» = null;''')
							} else {
								newLine
								append('''try {''')
								newLine
								append('''if(«p.name» != null) { synchronized(«p.name»_flow) { ''')
								newLine
								append('''while(!«p.name»_flow.offer(«p.name»)) {''')
								newLine
								append('''if(«p.name»_flow.isFinished()) { isFinished = true; return; }''')
								newLine
								append('''«p.name»_flow.wait(); }''')
								newLine
								append('''«p.name»_flow.notifyAll(); }''')
								newLine
								append('''«p.name» = null; }''')
								newLine
								append('''} catch (InterruptedException e) {e.printStackTrace();}''')
							}
						}
					]
					body = functionCode
				//annotations += stage.toAnnotation('''@Override''')
				]
				/* isFinished */
				if (stage.termination.size != 0) {
					members += stage.toMethod('''termination''', null)[body = stage.termination.head.expression]
					j = j + 1
				}
				members += stage.toMethod('''isFinished''', stage.newTypeRef("boolean")) [
					val Procedures.Procedure1<ITreeAppendable> functionCode = [
						append('''if(isFinished) return true;''')
						newLine
						if (stage.termination.size != 0) {
							append('''return termination();''')
						} else if (safeSubs.size > 0) {
							append(''' return counter == 1;''')
						} else {
							append(''' return counter == «upperExecBoarder»;''')
						}
					]
					body = functionCode
				//annotations += stage.toAnnotation('''@Override''')
				]
				/* teminate */
				members += stage.toMethod('''terminateNetwork''', stage.newTypeRef("void")) [
					val Procedures.Procedure1<ITreeAppendable> functionCode = [
						for (p : safePorts) {
							append(
								'''synchronized(«p.name»_flow) { «p.name»_flow.setFinished(true); «p.name»_flow.notifyAll(); }''')
						}
					]
					body = functionCode
				]
				/* Add subs and all their constructor arguments as final fields. */
				for (s : safeSubs) {
					try {
						val fieldType = typeReferences.getTypeForName(s.stage.fullyQualifiedName.toString, s,
							s.typeArgs)

						val subField = s.toField(s.name, fieldType)
						subField.final = true
						members += subField
						val c = Math::min(s.args.size, s.stage.params.size)
						for (i : 0 ..< c) {
							val a = s.args.get(i)
							val p = s.stage.params.get(i)
							val argField = a.toField(s.argumentFieldName(i), p.parameterType)
							argField.final = true
							argField.initializer = a
							members += argField
						}
					} catch (IllegalArgumentException ex) {
						/* Type cannot be created (getTypeForName(...)*/
						/* 
						 * Exception is silent, because illegal type-arguments in sf-files 
						 * are expected in general.
					     */
					}
				}
			])
	}

	def isSubFlow(Flow f, Iterable<Port> safePorts) {
		(f.src.sub == null && safePorts.exists[it.equals(f.src.port)]) ||
			(f.snk.sub == null && safePorts.exists[it.equals(f.snk.port)])
	}

	def findQueue(Flow f, Stage stage, Iterable<Port> safePorts) {
		if (f.src.sub == null && safePorts.exists[it.equals(f.src.port)]) //no stage identifier
		{
			('''«f.src.port.name»_flow''')
		} else if (f.snk.sub == null && safePorts.exists[it.equals(f.snk.port)]) //no stage identifier
		{
			('''«f.snk.port.name»_flow''')
		} else {
			('''«f.src.sub.name»_«f.src.port.name»TO«f.snk.sub.name»_«f.snk.port.name»_flow''')
		}
	}

	def argumentFieldName(Sub s, int index) {
		"_" + s.name + "_" + s.stage.params.get(index).name + "_arg"
	}

	def stepMethodName(Step s, int index) {
		"_execute_step_" + index
	}

}
